perf(gemma4): regular decode steps stop rebuilding what did not change - #1000
Conversation
Signed-off-by: Feathbow <feathbow@gmail.com> (cherry picked from commit 5c8c6c14165d03c90526a9e8886c25f07ae1581d) Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
…s them Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
…batch Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 872c5d70a4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| self.arena.invalidate_decode_fingerprint(); | ||
| self.drain_pipeline(active); |
There was a problem hiding this comment.
Avoid draining the decode pipeline when all slots are full
When concurrency exceeds self.slots, pending remains nonempty while active.len() == self.slots, so every engine-loop iteration reaches these calls even though the following admission loop cannot run. This invalidates the steady-decode metadata and drains the staged successor on every token, disabling both principal optimizations for the entire queued portion of a high-concurrency workload. Defer invalidation and draining until a slot is actually available and an admission can change the roster.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good Catch. Move the invalidation and clearing operations inside the loop, performing them only once before the first actual execution of admit_and_prefill.
| } | ||
| } | ||
|
|
||
| const DECODE_PIPELINE_DEPTH: usize = 2; |
There was a problem hiding this comment.
Document the new Gemma decode pipeline contract
This introduces a depth-two speculative decode pipeline, delayed token collection, and new drain/invalidation requirements, but the commit changes no Gemma living documentation. Record the production invariant, why the row order must remain pinned, and the conditions that drain the pipeline in docs/models/gemma4/; otherwise future scheduler work has no durable description of the constraint it must preserve.
AGENTS.md reference: AGENTS.md:L176-L178
Useful? React with 👍 / 👎.
… admit Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
|
@codex check again please |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ca46460e6c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| ## The decode pipeline | ||
|
|
||
| Greedy decode rounds run a depth-two software pipeline on the base stream: a step's argmax writes its picks straight into the id buffer the next step's embedding reads, its readback lands in one of two pinned slots, and the emitted token stream lags compute by one step. The production invariant is that token ids, finish reasons and token counts are identical to collecting every step at the same batch composition, and that the kernels inside a step and their order do not change. |
There was a problem hiding this comment.
Add production evidence for the decode fast path
This section asserts that the new pipelined sampler and device-side metadata advancement preserve token IDs, finish reasons, and counts, but the commit adds neither a production E2E/model evaluation for those output invariants nor same-context before/after performance results. Because the new path is exercised only for eligible greedy batches and crosses roster, page/chunk, cancellation, EOS, and length transitions, provide production coverage of those cases plus an A/B benchmark before treating the optimization as verified.
AGENTS.md reference: AGENTS.md:L110-L112
Useful? React with 👍 / 👎.
Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
…he roster Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
Description
Closes #999
The token read parked the caller on the stream every step and paid the OS scheduler's wake-up for it. It now spins on
cuStreamQueryfor a bounded window first, falling back to the parking wait so a stuck stream still sleeps.Greedy decode rounds run a depth-two software pipeline on the one stream: the staged argmax writes the picks into the id buffer the next step's embedding reads, the readback lands in one of two pinned slots, and the emitted stream lags compute by one step. Any composition change, non-greedy row, logprob request or row within two tokens of its length cap drains to the collect-every-step path; a stop found late marks the row stopping and the drain retires it. The kernels and their order do not change, and the emitted tokens are the same bytes.
A regular step re-uploaded every per-step metadata table although their next values were already known. A kernel captured at the decode graph's tail now advances them in place, and a step whose fingerprint matches the previous one — every length one token further, page, chunk and split structure untouched — skips the rebuild and every upload. Mixed steps, chunk boundaries, page turns and the precapture warm pass invalidate the fingerprint and rebuild as before.
The staged sampler chain — suppression, argmax and the device copy of the picks — is captured per bucket at startup and launched as one graph beside the decode replay; only the pinned readback stays outside, so the collector keeps the copy's own event.
Test Env
Single GPU (
sm_89, 48 GiB, x86_64)Verification
cargo fmt --all -- --checkandgit diff --checkagainstmainpass. The Gemma 4 release server builds, all-target Gemma/kernels/sample Clippy passes with-D warnings, and release lib tests pass: core 33, Gemma 42 with 20 ignored, kernels 16 with 1 ignored, sample 3.The engine and serving gates pass through
scripts/gemma4_gates.sh, eachrunning 1 test … 1 passedin its own process, the dual-checkpoint ones on both the 12B and the 26B: the shared and green lane lifecycles, the gathered lifecycle and the gathered walk against the serial path, the mixed step against the serial path, the overlapped prefill against the sync step, the ragged-batch row-order gate, and the 12B HF waypoint and greedy gates.Byte identity at pinned composition: the same four greedy public
/v1/completionsrequests (31-, 35- and 33-token prompts plus a 1421-token one; 64 forced tokens;finish_reason=length,completion_tokens=64) answer with one md5 onmain's binary and on this head's,219de1e3c09c.Serving A/B, this head against
main,vllm bench serverandom 1024 in / 128 out at 8 / 24 / 64 prompts, two cooled alternating rounds, same seed per round, mean [range]:mainmainMedian ITL follows TPOT (c1 8.51 → 8.38, c4 11.61 → 11.19, c16 16.31 → 16.07 ms). First-token time is not a claim of this change; one c4 round read a higher p99 on this branch (586.8 against 399.9 ms) that the other round did not (401.1 against 401.6 ms).
Node-level Nsight traces of the same request set (64 prompts at c16, then 8 at c1) on each binary: host-to-device copies 62,374 → 36,998 over the trace (the regular steps' metadata uploads gone; the bytes are the same 17.67 GB because the prefills' KV writes dominate), kernel launches 1,125,022 → 1,126,542 (one tail kernel and one sampler graph per step in place of the host-launched chain), inter-kernel gaps longer than 50 µs 1,695 → 641, summing 45.6 → 38.6 s over a trace span of 68.2 → 61.2 s with the GPU-busy total unchanged (22.49 → 22.51 s).